﻿namespace ImageDupFinder.ProgramCore
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Threading;
    using ImageDupFinder.PluginCore;

    /// <summary>
    /// Manage loading,calculation of similarity and other image related tasks
    /// </summary>
    public class ImageManager : IDisposable
    {
        #region private fields
        /// <summary>
        /// all loaded images
        /// </summary>
        private readonly Dictionary<string, ImageData> files;

        /// <summary>
        /// collection of Features to be extracted from image on loading
        /// </summary>
        private readonly List<IFeatureCollection> featuresCollections;

        /// <summary>
        /// all distances/similarity between images calculated using selected Features
        /// </summary>
        private readonly TriangleDictionary<ImageData, float> distances;

        /// <summary>
        /// files waiting for load
        /// </summary>
        private readonly Stack<string> fileTasks;

        /// <summary>
        /// images waiting for similarity calculation
        /// </summary>
        private readonly Stack<ImageData> imageTasks;

        /// <summary>
        /// Features used for similarity calculations
        /// </summary>
        private readonly List<string> selectedFeatures;

        /// <summary>
        /// loader threads
        /// </summary>
        private readonly Thread[] loaders;

        /// <summary>
        /// similarity calculating threads
        /// </summary>
        private readonly Thread[] calcs;

        /// <summary>
        /// duplicate finding thread
        /// </summary>
        private Thread dupFinder;

        /// <summary>
        /// loader threads progress
        /// </summary>
        private int loaderProgress = 0;

        /// <summary>
        /// calculation thread progress
        /// </summary>
        private int calcProgress = 0;

        /// <summary>
        /// number of loader threads not in blocking state
        /// </summary>
        private int busyLoaderThreads = 0;

        /// <summary>
        /// locking object used to pause thread
        /// </summary>
        private object loaderPauseLock = new object();

        /// <summary>
        /// Flag. States if loader is paused
        /// </summary>
        private bool isLoaderPaused = false;

        /// <summary>
        /// number of busy calc threads
        /// </summary>
        private int busyCalcThreads = 0;

        /// <summary>
        /// locking object used to pause similarity calculation
        /// </summary>
        private object calcPauseLock = new object();

        /// <summary>
        /// Flag. States if calc is paused
        /// </summary>
        private bool isCalcPaused = false;

        /// <summary>
        /// flag for synchronization of similarity calculation
        /// </summary>
        private int threadsCalculatingSim = 0;

        /// <summary>
        /// flag for calculating thread to raise "AllTasksCompleted" event  
        /// </summary>
        private int threadsProcessingImages = 0;

        /// <summary>
        /// locking object for Read/Write access to distances Dictionary
        /// </summary>
        private ReaderWriterLockSlim distancesLock = new ReaderWriterLockSlim();

        /// <summary>
        /// limit for dupfinder function
        /// </summary>
        private float dupfinderLimit;

        /// <summary>
        /// range for dupfinder function
        /// </summary>
        private float dupfinderRange;

        /// <summary>
        /// flag for canceling running dupfinder function
        /// </summary>
        private bool cancelDupfinderRequest = false;

        /// <summary>
        /// Object that is locked when dupfinding function is working.
        /// </summary>
        private object dupfinderLock = new object();

        /// <summary>
        /// Set to true after this object is disposed
        /// </summary>
        private bool isDiposed = false;

        #endregion

        /// <summary>
        /// Initializes a new instance of the ImageManager class.
        /// </summary>
        /// <param name="loaderThreads">number of image loading threads to create</param>
        public ImageManager(int loaderThreads)
        {
            int threadNum = (loaderThreads < 1) ? 1 : loaderThreads;
            this.files = new Dictionary<string, ImageData>();
            this.featuresCollections = new List<IFeatureCollection>();
            this.distances = new TriangleDictionary<ImageData, float>();
            this.fileTasks = new Stack<string>();
            this.imageTasks = new Stack<ImageData>();
            this.selectedFeatures = new List<string>();

            // create loader and calculator workers     
            // one thread per processor
            this.loaders = new Thread[threadNum];
            this.calcs = new Thread[threadNum];
            for (int i = 0; i < threadNum; i++)
            {
                // create loader worker
                Thread loader = new Thread(this.Loader_DoWork);
                this.loaders[i] = loader;
                loader.Name = "Loader - " + i;
                loader.IsBackground = true;
                loader.Start();

                // create calculator worker
                Thread calc = new Thread(this.Calc_DoWork);
                this.calcs[i] = calc;
                calc.Name = "Calc - " + i;
                calc.IsBackground = true;
                calc.Start();
            }

            // create duplicate finding worker
            this.dupFinder = new Thread(this.DupFinder_DoWork);
            this.dupFinder.Name = "DupFinder";
            this.dupFinder.IsBackground = true;
            this.dupFinder.Start();
        }

        /// <summary>
        /// Initializes a new instance of the ImageManager class using default number of loader threads.
        /// </summary>
        public ImageManager()
            : this(Environment.ProcessorCount)
        {
        }

        #region public events
        /// <summary>
        /// Occurs when image is loaded
        /// </summary>
        public event EventHandler<ProgressEventArgs> LoaderReportProgress;

        /// <summary>
        /// Occurs when similarity is calculated for image
        /// </summary>
        public event EventHandler<ProgressEventArgs> CalculatorReportProgress;

        /// <summary>
        /// Occurs when all running tasks finish processing images
        /// </summary>
        public event EventHandler AllTasksCompleted;

        /// <summary>
        /// Occurs when duplicate search finish work
        /// </summary>
        public event EventHandler<ImgSearchEventArgs> DuplicatesFound;
        #endregion

        #region public properties
        /// <summary>
        /// Gets number of processed files
        /// </summary>
        public int Count
        {
            get
            {
                int rv;
                this.distancesLock.EnterReadLock();
                try
                {
                    rv = this.distances.Keys.Count;
                }
                finally
                {
                    this.distancesLock.ExitReadLock();
                }

                return rv;
            }
        }

        /// <summary>
        /// Gets a value indicating whether this object was disposed.
        /// </summary>
        public bool IsDiposed
        {
            get { return this.isDiposed; }
        }

        /// <summary>
        /// Gets features used by by images
        /// </summary>
        public List<IFeatureCollection> FeaturesCollections
        {
            get { return this.featuresCollections; }
        }

        /// <summary>
        /// Gets all added images
        /// </summary>
        public Dictionary<string, ImageData> Files
        {
            get { return this.files; }
        }

        /// <summary>
        /// Gets number of loaded image files.
        /// </summary>
        public int FileCount
        {
            get
            {
                int rv;
                lock (this.files)
                {
                    rv = this.files.Count;
                }

                return rv;
            }
        }

        /// <summary>
        /// Gets number of files waiting in stack to be processed
        /// </summary>
        public int WaitingFilesCount
        {
            get
            {
                int rv;
                lock (this.fileTasks)
                {
                    rv = this.fileTasks.Count;
                }

                return rv;
            }
        }

        /// <summary>
        /// Gets number of images waiting in distance calculator stack 
        /// </summary>
        public int WaitingImagesCount
        {
            get
            {
                int rv;
                lock (this.imageTasks)
                {
                    rv = this.imageTasks.Count;
                }

                return rv;
            }
        }
        #endregion

        /// <summary>
        /// Clears file and image collections
        /// </summary>
        public void Clear()
        {
            // cancel running tasks
            this.StopFileLoading();
            this.StopSimilarityCalculation();

            // clear images
            this.distancesLock.EnterWriteLock();
            try
            {
                foreach (ImageData i in this.distances.Keys)
                {
                    i.Dispose();
                }

                this.distances.Clear();
            }
            finally
            {
                this.distancesLock.ExitWriteLock();
            }

            // clear files
            lock (this.files)
            {
                this.files.Clear();
            }
        }

        /// <summary>
        /// Finds duplicate images
        /// </summary>
        /// <param name="limit">similarity limit</param>
        /// <param name="range">similarity range</param>
        public void FindDups(float limit, float range)
        {
            // try to lock dupfinder
            if (!Monitor.TryEnter(this.dupfinderLock))
            {
                // if cant lock - dupfinder is runnig - cancel it
                this.cancelDupfinderRequest = true;
                Monitor.Enter(this.dupfinderLock);
            }

            this.dupfinderLimit = limit;
            this.dupfinderRange = range;
            this.cancelDupfinderRequest = false;
            Monitor.Pulse(this.dupfinderLock);
            Monitor.Exit(this.dupfinderLock);
        }

        /// <summary>
        /// Pauses image loading and processing
        /// </summary>
        public void Pause()
        {
            this.PauseFileLoading();
            this.PauseSimilarityCalculation();
        }

        /// <summary>
        /// Pause files loading
        /// </summary>
        /// <returns>false if file loading is already paused</returns>
        public bool PauseFileLoading()
        {
            if (this.isLoaderPaused)
            {
                return false;
            }

            lock (this.loaderPauseLock)
            {
                this.isLoaderPaused = true;
                while (this.busyLoaderThreads != 0)
                {
                    Monitor.Wait(this.loaderPauseLock);
                }
            }

            return true;
        }

        /// <summary>
        /// Pauses image similarity calculating
        /// </summary>
        /// <returns>false if already paused</returns>
        public bool PauseSimilarityCalculation()
        {
            if (this.isCalcPaused)
            {
                return false;
            }

            lock (this.calcPauseLock)
            {
                this.isCalcPaused = true;
                while (this.busyCalcThreads != 0)
                {
                    Monitor.Wait(this.calcPauseLock);
                }
            }

            return true;
        }

        /// <summary>
        /// Performs image analysis
        /// </summary>
        /// <param name="filePaths">List of files to be processed</param>
        /// <returns>number of added files</returns>
        public int ProcessFiles(IList<string> filePaths)
        {
            if (filePaths == null)
            {
                return 0;
            }

            List<string> tmp = new List<string>(filePaths.Distinct());

            lock (this.files)
                lock (this.fileTasks)
                {
                    for (int i = tmp.Count - 1; i >= 0; i--)
                    {
                        if (this.files.ContainsKey(tmp[i]))
                        {
                            tmp.RemoveAt(i);
                        }
                        else
                        {
                            this.files.Add(tmp[i], null);
                        }
                    }
                }

            if (tmp.Count != 0)
            {
                this.AddFiles(tmp.ToArray());
            }

            return tmp.Count;
        }

        /// <summary>
        /// Recalculates similarity between images
        /// </summary>
        public void RecalculateSimilarity()
        {
            bool paused = this.PauseFileLoading();
            this.StopSimilarityCalculation();

            this.distancesLock.EnterWriteLock();
            try
            {
                this.distances.Clear();
            }
            finally
            {
                this.distancesLock.ExitWriteLock();
            }

            lock (this.files)
            {
                lock (this.imageTasks)
                {
                    this.imageTasks.Clear();
                    foreach (ImageData i in this.files.Values)
                        if (i != null)
                        {
                            this.imageTasks.Push(i);
                        }

                    Monitor.PulseAll(this.imageTasks);
                }
            }

            if (paused)
            {
                this.ResumeFileLoading();
            }
        }

        /// <summary>
        /// Removes file from image collection
        /// </summary>
        /// <param name="f">file to remove</param>
        /// <returns>true - if processed file was successfully removed</returns>
        public bool RemoveFile(string f)
        {
            bool removed = false;
            lock (this.files)
            {
                ImageData toRem;
                if (this.files.TryGetValue(f, out toRem))
                {
                    this.files.Remove(f);
                    if (toRem != null)
                    {
                        this.distancesLock.EnterWriteLock();
                        try
                        {
                            this.distances.Remove(toRem);
                        }
                        finally
                        {
                            this.distancesLock.ExitWriteLock();
                        }

                        toRem.Dispose();
                        removed = true;
                    }
                }
            }

            return removed;
        }

        /// <summary>
        /// Removes image from similarity calculation results
        /// </summary>
        /// <param name="toRemove">image to remove</param>
        /// <returns>true if image was successfully removed</returns>
        public bool RemoveResult(ImageData toRemove)
        {
            bool removed = false;
            if (toRemove != null)
            {
                this.distancesLock.EnterWriteLock();
                try
                {
                    this.distances.Remove(toRemove);
                    removed = true;
                }
                finally
                {
                    this.distancesLock.ExitWriteLock();
                }
            }

            return removed;
        }

        /// <summary>
        /// Removes image from similarity calculation results
        /// </summary>
        /// <param name="file">file path to remove</param>
        /// <returns>true if image was successfully removed</returns>
        public bool RemoveResult(string file)
        {
            ImageData toRemove = null;
            lock (this.files)
                this.files.TryGetValue(file, out toRemove);
            return this.RemoveResult(toRemove);
        }

        /// <summary>
        /// Returns similarity search results as list.
        /// </summary>
        /// <returns>All similarities as list</returns>
        public List<SimilaritySearchResult> ResultsToList()
        {
            List<SimilaritySearchResult> list = new List<SimilaritySearchResult>();
            this.distancesLock.EnterReadLock();
            try
            {
                list.Capacity = this.distances.Keys.Count;
                foreach (ImageData key1 in this.distances.Keys)
                    foreach (ImageData key2 in this.distances[key1].Keys)
                    {
                        list.Add(new SimilaritySearchResult(key1.File, key2.File, this.distances[key1, key2]));
                    }
            }
            finally
            {
                this.distancesLock.ExitReadLock();
            }

            return list;
        }

        /// <summary>
        /// Resumes file loading and image processing
        /// </summary>
        public void Resume()
        {
            this.ResumeFileLoading();
            this.ResumeSimilarityCalculation();
        }

        /// <summary>
        /// Resumes image files loading
        /// </summary>
        /// <returns>true if loading was resumed</returns>
        public bool ResumeFileLoading()
        {
            if (!this.isLoaderPaused)
            {
                return false;
            }

            lock (this.fileTasks)
            {
                this.isLoaderPaused = false;
                Monitor.PulseAll(this.fileTasks);
            }

            return true;
        }

        /// <summary>
        /// Resume similarity calculation
        /// </summary>
        /// <returns>true if similarity calculation was resumed</returns>
        public bool ResumeSimilarityCalculation()
        {
            if (!this.isCalcPaused)
            {
                return false;
            }

            lock (this.imageTasks)
            {
                this.isCalcPaused = false;
                Monitor.PulseAll(this.imageTasks);
            }

            return true;
        }

        /// <summary>
        /// Sets Selected features to use by this ImageManager.
        /// Pass null as argument to clear selected features.
        /// </summary>
        /// <param name="featurelist">feature list</param>
        public void SetSelectedFeatures(IEnumerable<string> featurelist)
        {
            lock (this.selectedFeatures)
            {
                this.selectedFeatures.Clear();
                if (featurelist != null)
                {
                    this.selectedFeatures.AddRange(featurelist);
                }
            }
        }

        /// <summary>
        /// Clear and stop file queue
        /// </summary>
        public void StopFileLoading()
        {
            this.PauseFileLoading();

            // clear queue
            lock (this.fileTasks)
            {
                this.fileTasks.Clear();
                this.isLoaderPaused = false;
            }
        }

        /// <summary>
        /// Clear and stop image queue
        /// </summary>
        public void StopSimilarityCalculation()
        {
            this.PauseSimilarityCalculation();

            // clear queue
            lock (this.imageTasks)
            {
                this.imageTasks.Clear();
                this.isCalcPaused = false;
            }
        }

        #region IDisposable Members
        /// <summary>
        ///  Releases all resources used by this ImageManager.
        /// </summary>
        public void Dispose()
        {
            if (this.isDiposed)
            {
                return;
            }

            lock (this.fileTasks)
            {
                this.fileTasks.Clear();
                this.isLoaderPaused = false;

                // push null
                foreach (Thread t in this.loaders)
                {
                    this.fileTasks.Push(null);
                }

                Monitor.PulseAll(this.fileTasks);
            }

            lock (this.imageTasks)
            {
                this.imageTasks.Clear();
                this.isCalcPaused = false;
                this.imageTasks.Push(null);
                Monitor.PulseAll(this.imageTasks);
            }

            lock (this.dupfinderLock)
            {
                this.dupfinderLimit = -1;
                Monitor.PulseAll(this.dupfinderLock);
            }

            // wait for workers to finish
            foreach (Thread t in this.loaders)
            {
                t.Join();
            }

            foreach (Thread t in this.calcs)
            {
                t.Join();
            }

            this.dupFinder.Join();

            // Dispose images
            foreach (ImageData i in this.files.Values)
            {
                if (i != null)
                {
                    i.Dispose();
                }
            }

            this.isDiposed = true;
        }

        #endregion

        /// <summary>
        /// Calculates similarity between provided image and all already processed images
        /// </summary>
        /// <param name="image">ImageData that represents image used in similarity calculation</param>
        /// <returns>Similarities for provided images</returns>
        internal Dictionary<ImageData, float> GetSimilarity(ImageData image)
        {
            Dictionary<ImageData, float> tempDist = new Dictionary<ImageData, float>();
            float similarity;

            this.distancesLock.EnterReadLock();
            try
            {
                lock (this.selectedFeatures)
                {
                    foreach (ImageData im in this.distances.Keys)
                    {
                        similarity = 0;
                        foreach (string f in this.selectedFeatures)
                        {
                            similarity += image.CalcualteSimilarity(im, f);
                        }

                        similarity /= this.selectedFeatures.Count;

                        // add only similar images   
                        if (similarity < 1)
                        {
                            tempDist[im] = similarity;
                        }
                    }
                }
            }
            finally
            {
                this.distancesLock.ExitReadLock();
            }

            return tempDist;
        }

        #region worker threads
        /// <summary>
        /// main method of duplicate finding thread
        /// </summary>
        protected void DupFinder_DoWork()
        {
            List<List<ImageData>> list;
            while (true)
            {
                lock (this.dupfinderLock)
                {
                    Monitor.Wait(this.dupfinderLock);
                    if (this.dupfinderLimit < 0)
                    {
                        return; // exit on limit < 0
                    }

                    list = new List<List<ImageData>>();

                    this.distancesLock.EnterReadLock();
                    try
                    {
                        foreach (KeyValuePair<ImageData, Dictionary<ImageData, float>> outer in this.distances)
                        {
                            if (this.cancelDupfinderRequest)
                            {
                                break;
                            }

                            foreach (KeyValuePair<ImageData, float> inner in outer.Value)
                            {
                                if (inner.Value <= this.dupfinderLimit &&
                                    (inner.Value >= this.dupfinderLimit - this.dupfinderRange))
                                {
                                    this.AddImageToGroup(list, outer.Key, inner.Key);
                                }
                            }
                        }
                    }
                    finally
                    {
                        this.distancesLock.ExitReadLock();
                    }
                }

                if (!this.cancelDupfinderRequest)
                {
                    this.DupFinder_RaiseWorkCompletedEvent(list);
                }
            }
        }

        /// <summary>
        /// main method of loader threads
        /// </summary>
        protected void Loader_DoWork()
        {
            int fc;
            ImageData img;

            this.busyLoaderThreads++;
            while (true)
            {
                string task = null;
                lock (this.fileTasks)
                {
                    while (this.fileTasks.Count == 0 || this.isLoaderPaused)
                    {
                        lock (this.loaderPauseLock)
                        {
                            this.busyLoaderThreads--;                    // decrease number of busy threads
                            // test if thread was paused
                            if (this.isLoaderPaused)
                            {
                                Monitor.Pulse(this.loaderPauseLock);     // send signal back to thread that requested pause
                            }
                        }

                        Monitor.Wait(this.fileTasks);                     // wait for signal 
                        this.busyLoaderThreads++;                        // increase number of busy threads
                    }

                    this.threadsProcessingImages++;                      // increase number of threads processing images
                    task = this.fileTasks.Pop();
                }

                // on null task exit thread
                if (task == null)
                {
                    this.threadsProcessingImages--;
                    this.busyLoaderThreads--;
                    return;
                }

                // begin processing file
                lock (this.files)
                {
                    // test if file exist
                    if (this.files.TryGetValue(task, out img))
                    {
                        if (img == null)
                        {
                            img = new ImageData(task);
                            this.files[task] = img;
                        }
                    }
                    else
                    {
                        this.threadsProcessingImages--;
                        continue;
                    }
                }

                // calculate features
                img.CalculateFeatures(this.featuresCollections);          // time consuming work happens here
                this.threadsProcessingImages--;

                // add to calc queue
                this.AddImage(img);

                // report progress 
                fc = this.FileCount;
                int progress = (fc - this.WaitingFilesCount) * 100 / fc;
                if (progress != this.loaderProgress)
                {
                    this.loaderProgress = progress;
                    this.Loader_RaiseReportProgressEvent(progress);
                }
            }
        }

        /// <summary>
        /// main method of distance calculator thread
        /// </summary>
        protected void Calc_DoWork()
        {
            int progress;
            int calculatedImagesCount;
            Dictionary<ImageData, float> tmpSim;

            this.busyCalcThreads++;
            while (true)
            {
                ImageData task = null;
                lock (this.imageTasks)
                {
                    while (this.imageTasks.Count == 0 || this.isCalcPaused)
                    {
                        this.busyCalcThreads--;                        // decrease number of busy threads
                        lock (this.calcPauseLock)
                        {
                            if (this.isCalcPaused)
                            {                                           // test if thread was paused
                                Monitor.Pulse(this.calcPauseLock);     // send signal back to thread that requested pause
                            }
                        }

                        Monitor.Wait(this.imageTasks);                  // wait for signal 
                        this.busyCalcThreads++;                        // increase number of busy threads
                    }

                    this.threadsCalculatingSim++;                      // increase number of threads calculating similarity
                    task = this.imageTasks.Pop();
                }

                // on null task exit thread
                if (task == null)
                {
                    this.threadsCalculatingSim--;
                    this.busyCalcThreads--;
                    return;
                }

                // calculate similarity
                tmpSim = this.GetSimilarity(task);

                // save data and sync with other threads 
                if (this.threadsCalculatingSim <= 1)
                {
                    this.distancesLock.EnterWriteLock();
                    try
                    {
                        this.distances[task] = tmpSim;
                    }
                    finally
                    {
                        this.distancesLock.ExitWriteLock();                        
                    }
                }
                else
                {
                    this.distancesLock.EnterWriteLock();
                    try
                    {
                            lock (this.selectedFeatures)
                            {
                                float similarity = 0;
                                ImageData lastImg = this.distances.Keys.Last();
                                foreach (string f in this.selectedFeatures)
                                {
                                    similarity += task.CalcualteSimilarity(lastImg, f);
                                }

                                similarity /= this.selectedFeatures.Count;

                                // add only similar images   
                                if (similarity < 1)
                                {
                                    tmpSim[lastImg] = similarity;
                                }
                            }

                        this.distances[task] = tmpSim;
                    }
                    finally
                    {
                        this.distancesLock.ExitWriteLock();
                    }
                }

                this.threadsCalculatingSim--;       // reduce number of threads calculating similarity

                this.distancesLock.EnterReadLock();
                try
                {
                    calculatedImagesCount = this.distances.Keys.Count;
                }
                finally
                {
                    this.distancesLock.ExitReadLock();
                }

                progress = calculatedImagesCount * 100 / this.FileCount;

                // report progress 
                if (progress != this.calcProgress)
                {
                    this.calcProgress = progress;
                    this.Calc_RaiseReportProgressEvent(progress);
                }

                // report job finished
                if (progress == 100
                    && this.WaitingFilesCount == 0 && this.WaitingImagesCount == 0
                    && this.threadsProcessingImages == 0 && this.threadsCalculatingSim == 0)
                {
                    this.Calc_RaiseAllTasksCompletedEvent();
                }
            }
        }
        #endregion

        #region private functions
        /// <summary>
        /// Adds image files to stack
        /// </summary>
        /// <param name="fileNames">files to load</param>
        private void AddFiles(string[] fileNames)
        {
            lock (this.fileTasks)
            {
                foreach (string f in fileNames)
                {
                    this.fileTasks.Push(f);
                }

                // unblock threads
                if (fileNames.Length < this.loaders.Length)
                {
                    for (int i = 0; i < fileNames.Length; i++)
                    {
                        Monitor.Pulse(this.fileTasks);
                    }
                }
                else
                {
                    Monitor.PulseAll(this.fileTasks);
                }
            }
        }

        /// <summary>
        /// Add image to distance calculator stack
        /// </summary>
        /// <param name="image">image used in similarity calculation</param>
        private void AddImage(ImageData image)
        {
            lock (this.imageTasks)
            {
                this.imageTasks.Push(image);
                Monitor.Pulse(this.imageTasks);
            }
        }

        /// <summary>
        /// Image grouping function.
        /// </summary>
        /// <param name="list">List of image groups</param>
        /// <param name="img1">first image</param>
        /// <param name="img2">second image</param>
        private void AddImageToGroup(List<List<ImageData>> list, ImageData img1, ImageData img2)
        {
            List<ImageData> addHere = null;
            ImageData addWhat = null;
            bool cont1, cont2;

            foreach (List<ImageData> l in list)
            {
                cont1 = l.Contains(img1);
                cont2 = l.Contains(img2);

                if (cont1 && cont2)
                {
                    return;
                }

                if ((cont1 || cont2) && addHere != null)
                {
                    return;
                }

                if (cont1 && !cont2)
                {
                    addHere = l;
                    addWhat = img2;
                    continue;
                }

                if (cont2 && !cont1)
                {
                    addHere = l;
                    addWhat = img1;
                    continue;
                }
            }

            if (addWhat != null)
            {
                addHere.Add(addWhat);
            }
            else
            {
                List<ImageData> nl = new List<ImageData>();
                nl.Add(img1);
                nl.Add(img2);
                list.Add(nl);
            }
        }
        #endregion

        #region cross thread event invocation
        /// <summary>
        /// Raises DuplicatesFound Event.
        /// Runs after duplicate finding thread finish its task.
        /// </summary>
        /// <param name="result">duplicate search result</param>
        private void DupFinder_RaiseWorkCompletedEvent(List<List<ImageData>> result)
        {
            ImgSearchEventArgs isea = new ImgSearchEventArgs(result);
            if (this.DuplicatesFound != null)
            {
                object[] args = new object[] { this, isea };
                foreach (Delegate d in this.DuplicatesFound.GetInvocationList())
                {
                    ISynchronizeInvoke isi = d.Target as ISynchronizeInvoke;
                    if (isi != null && isi.InvokeRequired)
                    {
                        isi.BeginInvoke(d, args);
                    }
                    else
                    {
                        d.DynamicInvoke(args);
                    }
                }
            }
        }

        /// <summary>
        /// Raises AllTasksCompleted event.
        /// Runs after distance calculator thread finish all tasks
        /// </summary>
        private void Calc_RaiseAllTasksCompletedEvent()
        {
            if (this.AllTasksCompleted != null)
            {
                object[] args = new object[] { this, new EventArgs() };
                foreach (Delegate d in this.AllTasksCompleted.GetInvocationList())
                {
                    ISynchronizeInvoke isi = d.Target as ISynchronizeInvoke;
                    if (isi != null && isi.InvokeRequired)
                    {
                        isi.BeginInvoke(d, args);
                    }
                    else
                    {
                        d.DynamicInvoke(args);
                    }
                }
            }
        }

        /// <summary>
        /// Raises Calculator Report Progress event.
        /// Runs after distance calculator thread report its progress.
        /// </summary>
        /// <param name="progress">new progress value</param>
        private void Calc_RaiseReportProgressEvent(int progress)
        {
            if (this.CalculatorReportProgress != null)
            {
                object[] args = new object[] { this, new ProgressEventArgs(progress) };
                foreach (Delegate d in this.CalculatorReportProgress.GetInvocationList())
                {
                    ISynchronizeInvoke isi = d.Target as ISynchronizeInvoke;
                    if (isi != null && isi.InvokeRequired)
                    {
                        isi.BeginInvoke(d, args);
                    }
                    else
                    {
                        d.DynamicInvoke(args);
                    }
                }
            }
        }

        /// <summary>
        /// Raises Loader report progress event.
        /// Runs after one of loader threads reports progress.
        /// </summary>
        /// <param name="progress">new progress value</param>
        private void Loader_RaiseReportProgressEvent(int progress)
        {
            if (this.LoaderReportProgress != null)
            {
                object[] args = new object[] { this, new ProgressEventArgs(progress) };
                foreach (Delegate d in this.LoaderReportProgress.GetInvocationList())
                {
                    ISynchronizeInvoke isi = d.Target as ISynchronizeInvoke;
                    if (isi != null && isi.InvokeRequired)
                    {
                        isi.BeginInvoke(d, args);
                    }
                    else
                    {
                        d.DynamicInvoke(args);
                    }
                }
            }
        }
        #endregion
    }

    #region helper classes
    /// <summary>
    /// Class containing duplicate search event data
    /// </summary>
    public class ImgSearchEventArgs : EventArgs
    {
        /// <summary>
        /// Duplicate search result
        /// </summary>
        private readonly List<List<ImageData>> result;

        /// <summary>
        /// Initializes a new instance of the ImgSearchEventArgs class.
        /// </summary>
        /// <param name="result">Search result</param>
        public ImgSearchEventArgs(List<List<ImageData>> result)
        {
            this.result = result;
        }

        /// <summary>
        /// Gets image search result.
        /// </summary>
        public List<List<ImageData>> Result
        {
            get { return this.result; }
        }
    }

    /// <summary>
    /// Class containing progress event data
    /// </summary>
    public class ProgressEventArgs : EventArgs
    {
        /// <summary>
        /// Event progress
        /// </summary>
        private readonly int progress;

        /// <summary>
        /// Initializes a new instance of the ProgressEventArgs class.
        /// </summary>
        /// <param name="progress">new progress to report</param>
        public ProgressEventArgs(int progress)
        {
            this.progress = progress;
        }

        /// <summary>
        /// Gets event progress.
        /// </summary>
        public int Progress
        {
            get { return this.progress; }
        }
    }

    /// <summary>
    /// Class holding single Similarity search result
    /// </summary>
    public class SimilaritySearchResult
    {
        /// <summary>
        /// File paths
        /// </summary>
        private readonly string f1, f2;

        /// <summary>
        /// Calculated similarity
        /// </summary>
        private readonly float similarity;

        /// <summary>
        /// Initializes a new instance of the SimilaritySearchResult class.
        /// </summary>
        /// <param name="f1">First file</param>
        /// <param name="f2">Second file</param>
        /// <param name="similarity">Similarity between files</param>
        public SimilaritySearchResult(string f1, string f2, float similarity)
        {
            this.f1 = f1;
            this.f2 = f2;
            this.similarity = similarity;
        }

        /// <summary>
        /// Gets first file
        /// </summary>
        public string File1
        {
            get { return this.f1; }
        }

        /// <summary>
        /// Gets second file
        /// </summary>
        public string File2
        {
            get { return this.f2; }
        }

        /// <summary>
        /// Gets similarity between files
        /// </summary>
        public float Similarity
        {
            get { return this.similarity; }
        }
    }

    #endregion
}
